/*
 * Copyright (c) 2014 Luca Agostini, Alessandro Cantini, Alessandro Dignani
 *
 * This file is part of MeteoCal.
 *
 * MeteoCal is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package it.polimi.se2.agostini_cantini_dignani.meteocal.business;

import it.polimi.se2.agostini_cantini_dignani.meteocal.entity.Calendar;
import it.polimi.se2.agostini_cantini_dignani.meteocal.entity.Event;
import it.polimi.se2.agostini_cantini_dignani.meteocal.entity.User;
import it.polimi.se2.agostini_cantini_dignani.meteocal.entity.WeatherForecast;

import javax.ejb.Asynchronous;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * This class manages the business logic related to events.
 *
 * @author Alessandro Cantini
 */
@Stateless
public class EventManager {

	@PersistenceContext
	private EntityManager em;

	@EJB
	private CalendarManager cm;

	@EJB
	private NotificationManager nm;

	@EJB
	private WeatherForecastManager wfm;

	/**
	 * Creates an event into a user's calendar. The user will be the owner and will participate to
	 * the event.
	 *
	 * @param event the event to add
	 * @param owner the creator of the event
	 */
	public void createEvent(Event event, User owner) {
		boolean wasSetQuick = false;
		event.setOwner(owner);
		wasSetQuick = setQuickWeather(event);
		em.persist(event);
		em.flush();
		addParticipation(event, owner.getCalendar());

		if (!wasSetQuick) {
			setAsynchronousWeather(event);
		}
	}

	/**
	 * Makes a user's calendar participate to an event
	 *
	 * @param event    the event to participate
	 * @param calendar the user's calendar
	 */
	public void addParticipation(Event event, Calendar calendar) {
		calendar.getEvents().add(event);
		event.getParticipantCalendars().add(calendar);
		cm.editCalendar(calendar);
	}

	/**
	 * Removes the participation of a user's calendar from a event. If the event has no more
	 * participant users or invited users, it will be deleted from database.
	 *
	 * @param event    the event to remove
	 * @param calendar the calendar that contains the event
	 */
	public void removeParticipation(Event event, Calendar calendar) {
		event.getParticipantCalendars().remove(calendar);
		calendar.getEvents().remove(event);

		cm.editCalendar(calendar);

		if (event.getParticipantCalendars().isEmpty() && event.getInvitedUsers().isEmpty()) {
			em.remove(em.merge(event));
		}
	}

	/**
	 * Finds an event in the database with a specified ID.
	 *
	 * @param id the id of the event
	 * @return the event with the specified ID
	 */
	public Event findById(Integer id) {
		return em.find(Event.class, id);
	}

	/**
	 * Edits the event in the database, updating the weather forecast.
	 *
	 * @param event the event to edit
	 */
	public void editEvent(Event event, boolean checkWeather) {
		boolean wasSetQuick = false;
		if (checkWeather) {
			wasSetQuick = setQuickWeather(event);
		}

		Event old = event;
		em.merge(event);
		old = event;

		if (!wasSetQuick && checkWeather) {
			setAsynchronousWeather(event);
		}
	}

	/**
	 * Returns the list of all the events ot the program.
	 *
	 * @return the list of all the events
	 */
	public List<Event> findAll() {
		TypedQuery<Event> query = em.createNamedQuery(Event.findAll, Event.class);
		return query.getResultList();
	}

	/**
	 * Invites a specified user to a specified event
	 *
	 * @param event the event to which the user is invited
	 * @param user  the user to invite
	 */
	public void inviteUserToEvent(Event event, User user) {
		if (event.getInvitedUsers() == null) {
			event.setInvitedUsers(new ArrayList<>());
		}
		if (canBeInvited(user.getEmail(), event, event.getOwner())) {
			event.getInvitedUsers().add(user);
		}
		editEvent(event, false);
		nm.sendInvitationNotification(event.getOwner(), user, event);
	}

	/**
	 * Removes an invitation of a specified user from a specified event.
	 *
	 * @param event the event from which the invitation is removed
	 * @param user  the user that was invited
	 */
	private void removeInvitation(Event event, User user) {
		event.getInvitedUsers().remove(user);
	}

	/**
	 * Tells if a specified email address belongs to a user who can be invited to a specified
	 * event.
	 *
	 * @param email the email to test
	 * @param event the event to which the user could be invited
	 * @return true if the email belongs to a user that can be invited to the specified event
	 */
	public boolean canBeInvited(String email, Event event, User inviter) {
		if (email == null) {
			return false;
		}
		User user = em.find(User.class, email);

		return user != null
				&& !user.equals(inviter)
				&& !user.equals(event.getOwner())
				&& !event.getParticipantCalendars().contains(user.getCalendar())
				&& !event.getInvitedUsers().contains(user)
				&& !event.getUsersWhoHaveDeclined().contains(user);
	}

	/**
	 * Makes a user accept the invitation to an event.
	 *
	 * @param user  the user who is invited.
	 * @param event the event to which the user was invited.
	 */
	public void acceptInvitation(User user, Event event) {
		removeInvitation(event, user);
		addParticipation(event, user.getCalendar());
		editEvent(event, false);
	}

	/**
	 * Makes a user decline the invitation to an event.
	 *
	 * @param user  the user who declines the invitation to an event.
	 * @param event the event to which the user was invited.
	 */
	public void declineInvitation(User user, Event event) {
		removeInvitation(event, user);
		event.getUsersWhoHaveDeclined().add(user);
		editEvent(event, false);
	}

	/**
	 * Compares two events, and returns true if they are the same. To be considered the same, two
	 * events must have the same title, description, place, start and end. Outdoor and public fields
	 * are not considered.
	 *
	 * @param e1 the first event
	 * @param e2 the second event
	 * @return true if two events are the same, false otherwise
	 */
	public boolean areTheSame(Event e1, Event e2) {
		if (e1 == null || e2 == null) {
			return false;
		}

		return sameFields(e1.getDescription(), e2.getDescription())
				&& sameFields(e1.getPlace(), e2.getPlace())
				&& e1.getTitle().equals(e2.getTitle())
				&& e1.getStartTime().equals(e2.getStartTime())
				&& e1.getEndTime().equals(e2.getEndTime());
	}

	/**
	 * Moves the event to a new date
	 *
	 * @param event   the event whose date must be changed
	 * @param newDate the new date
	 */
	public void moveEvent(Event event, Date newDate) {
		long duration = getDuration(event);
		event.setStartTime(newDate);
		event.setEndTime(new Date(newDate.getTime() + duration));
		editEvent(event, true);
	}

	/**
	 * Tells if a calendar contains events that would overlapping with the given event.
	 *
	 * @param event    the given event
	 * @param calendar the calendar to check
	 * @return true if the event contains overlapping events
	 */
	public boolean thereAreOverlappingEvents(Event event, Calendar calendar) {
		if (event == null || calendar == null || calendar.getEvents() == null) {
			throw new IllegalArgumentException();
		}

		for (Event e : calendar.getEvents()) {
			if (overlappingEvents(event, e) && (event.getEventId() == null || !event.equals(e))) {
				return true;
			}
		}
		return false;
	}

	@Asynchronous
	public void setAsynchronousWeather(Event event) {
		event.setWeatherForecast(wfm.getForecast(event.getPlace(), event.getStartTime(), false));
		editEvent(event, false);
	}

	private boolean setQuickWeather(Event event) {
		WeatherForecast wf = wfm.getForecast(event.getPlace(), event.getStartTime(), true);
		if (wf != null) {
			event.setWeatherForecast(wf);
			return true;
		}
		return false;
	}

	/**
	 * Tells if two events are overlapping.
	 *
	 * @param e1 the first event
	 * @param e2 the second event
	 * @return true if two events are overlapping
	 */
	protected boolean overlappingEvents(Event e1, Event e2) {
		if (e1 == null || e2 == null) {
			throw new IllegalArgumentException();
		}

		return areOverlapping(e1, e2) || areOverlapping(e2, e1);
	}

	private boolean areOverlapping(Event e1, Event e2) {
		return !e2.getStartTime().before(e1.getStartTime()) &&
				e1.getEndTime().after(e2.getStartTime());
	}

	private long getDuration(Event event) {
		return event.getEndTime().getTime() - event.getStartTime().getTime();
	}

	private boolean sameFields(Object f1, Object f2) {
		if (f1 == null) {
			return f2 == null;
		}
		return f1.equals(f2);
	}
}
